package xyz.xtt.asset.service.impl;

import static java.util.stream.Collectors.toList;
import static xyz.xtt.common.asset.AssertUtils.isFalse;
import static xyz.xtt.common.asset.AssertUtils.isNull;
import static xyz.xtt.common.asset.AssertUtils.isTrue;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import xyz.xtt.asset.api.enums.AssetTypeEnum;
import xyz.xtt.asset.api.enums.BizTypeEnum;
import xyz.xtt.asset.api.enums.ShowStateEnum;
import xyz.xtt.asset.client.request.MintDataVo;
import xyz.xtt.asset.client.request.MintRequest;
import xyz.xtt.asset.client.response.BurnResponse;
import xyz.xtt.asset.client.response.MintResponse;
import xyz.xtt.asset.constants.ErrorMessage;
import xyz.xtt.asset.entity.TblAsset;
import xyz.xtt.asset.entity.TblAssetExt;
import xyz.xtt.asset.entity.TblAssetStaticResource;
import xyz.xtt.asset.entity.TblBlindBoxDetail;
import xyz.xtt.asset.enums.OrderTypeEnum;
import xyz.xtt.asset.request.BurnRequest;
import xyz.xtt.asset.service.IBlindBoxService;
import xyz.xtt.asset.service.INftService;
import xyz.xtt.asset.service.ITblAssetExtService;
import xyz.xtt.asset.service.ITblAssetService;
import xyz.xtt.asset.service.ITblAssetStaticResourceService;
import xyz.xtt.asset.service.ITblBlindBoxDetailService;
import xyz.xtt.asset.vo.MetaDataParams;
import xyz.xtt.common.starter.cache.CacheManager;
import xyz.xtt.pay.api.dto.PayAttach;
import xyz.xtt.pay.client.response.TblPayOrderDto;

@Slf4j
@RefreshScope
@Service
public class BlindBoxServiceImpl implements IBlindBoxService {
	@Autowired
	private CacheManager cacheManager;
	@Autowired
	private ITblAssetService iTblAssetService;
	@Autowired
	private ITblBlindBoxDetailService iTblBlindBoxDetailService;

	@Value("${asset.blindbox.fakemin:5}")
	private Long fakeMin;

	@Value("${asset.blindbox.fakemax:8}")
	private Long fakeMax;

	@Value("${asset.blindbox.actlimit:1000}")
	private Long actLimit;

	@Autowired
	private ITblAssetExtService iTblAssetExtService;

	@Autowired
	private ITblAssetStaticResourceService iTblAssetStaticResourceService;

	@Autowired
	private INftService iNftService;

	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@Autowired
	private Environment environment;

	private void showLog(Long paryOrderId, String goodsId, Long appUid) {
		log.info("BlindBox paryOrderId:{},goodsId:{},appUid:{}", paryOrderId, goodsId, appUid);
	}

	@Transactional(rollbackFor = Exception.class)
	@Override
	public boolean openBox(TblPayOrderDto payOrderDto) {
		Long paryOrderId = payOrderDto.getPayOrderId();
		String orderNo = payOrderDto.getBusinessRefId();
		Long appUid = payOrderDto.getAppUserId();
		PayAttach payAttach = JSONUtil.toBean(payOrderDto.getBusinessAttach(), PayAttach.class);
		isNull(payAttach, ErrorMessage.PAY_ATTACH_ISNULL);
		String goodsId = payAttach.getGoodsId();
		TblAsset asset = iTblAssetService.info(goodsId);
		int purchaseNum = purchaseNum(payOrderDto.getBusinessAttach());
		if (Objects.isNull(asset)) {
			showLog(paryOrderId, goodsId, appUid);
			return Boolean.FALSE;
		}

		Long assetId = asset.getId();
		boolean openFake = isOpenFake(Longs.tryParse(asset.getGoodsId()));
		TblAssetExt assetExt = iTblAssetExtService.infoWithAssetIdByCache(assetId);
		isFalse(assetExt.isErc721(), ErrorMessage.TEMPLATE_MINT_WITH_721, () -> log
				.info("assetExt aid:{},protocol:{}", assetExt.getAssetId(), assetExt.getReferProtocol()));
		log.info("openBox paryOrderId:{} purchaseNum:{}", paryOrderId, purchaseNum);
		BurnResponse burnResult = null;
		try {
			BurnRequest burnRequest = BurnRequest.builder().ownerId(asset.getOwnerId()).bizId(goodsId).amount(purchaseNum).build();
			burnResult = iNftService.burn(BizTypeEnum.NFT.dbType(), burnRequest);
		} catch (Exception e) {
			log.error("iNftService.burn failed. paryOrderId:{} purchaseNum:{}", paryOrderId, purchaseNum, e);
		}
		log.info("openBox paryOrderId:{} purchaseNum:{},burn:{}", paryOrderId, purchaseNum, burnResult);
		for (int i = 0; i < purchaseNum; i++) {
			String fileUri = fileUri(asset);
			MintResponse response = templateMint(openFake, appUid, orderNo, asset, assetExt, fileUri);
			isNull(response, ErrorMessage.BLIND_BOX_OPEN_EX);
			Long tid = response.getTransactionOrderId();
			Long seqNo = response.getSeqNo();
			log.info("openBox paryOrderId:{}, purchaseNum:{}, response tid:{}, seq:{}", paryOrderId, purchaseNum, tid, seqNo);
			templateMintAfter(openFake, Longs.tryParse(asset.getGoodsId()), response, paryOrderId);
		}

		return true;
	}

	private int purchaseNum(String businessAttach) {
		// 购买详情
		PayAttach payAttach = JSONUtil.toBean(businessAttach, PayAttach.class);
		isNull(payAttach, ErrorMessage.PAY_ATTACH_ISNULL);
		// 购买数量
		Integer purchaseNum = payAttach.getPurchaseNum();
		isNull(purchaseNum, ErrorMessage.PAY_ATTACH_ISNULL);
		isTrue(purchaseNum.intValue() <= 0, ErrorMessage.PURCHASE_NUM_EX, () -> log.info("businessAttach:{}", businessAttach));
		return purchaseNum.intValue();
	}

	private void templateMintAfter(boolean openFake, Long parentId, MintResponse response, Long paryOrderId) {
		Long seqNo = response.getSeqNo();
// 		Long transactionOrderId = response.getTransactionOrderId();
// 		boolean mintAfterUpdateResult = iTblPayOrderService.mintAfterUpdateResult(paryOrderId, transactionOrderId);
// 		isFalse(mintAfterUpdateResult, ErrorMessage.MINT_EX, () -> log.info("transactionOrderId:{}", transactionOrderId));
// 		log.info("openBox paryOrderId:{}, transactionOrderId:{},mintAfterUpdateResult:{}", paryOrderId, transactionOrderId, mintAfterUpdateResult);
		// 超过1000, 用直实的销量,不在虚增销量
		Long fakeLimit = fakeLimit(parentId);
		if (seqNo.longValue() > fakeLimit) {
			iTblAssetService.updateSeqNo(parentId, 1L);
		} else {
			boolean fake = openFake && fakeMax.longValue() > fakeMin.longValue();
			if (fake) {
				Long fakeRdm = ThreadLocalRandom.current().nextLong(fakeMin, fakeMax);
				Long fakeVal = 1 + fakeRdm;
				iTblAssetService.updateSeqNo(parentId, fakeVal);
			} else {
				iTblAssetService.updateSeqNo(parentId, 1L);
			}
		}
	}

	private Long fakeLimit(Long parentId) {
		String fakeNasLimitConfig = String.format("asset.%d.fakeLimit", parentId);
		Long fakeNasLimitVal = environment.getProperty(fakeNasLimitConfig, Long.class, 0L);
		return fakeNasLimitVal;
	}

	private boolean isOpenFake(Long parentId) {
		String fakeNasConfig = String.format("asset.%d.fakeOpen", parentId);
		// 如果开启了假数据
		Boolean fakeNasOpenVal = environment.getProperty(fakeNasConfig, Boolean.class, Boolean.FALSE);
		return Objects.equals(Boolean.TRUE, fakeNasOpenVal);
	}

	private MintResponse templateMint(boolean openFake, Long appUid, String orderNo, TblAsset asset, TblAssetExt assetExt, String fileUri) {
		// 原类型是盲盒，现有的类型不能是盲盒
		List<MintDataVo> mintAttr = mintAttr(assetExt.getMetadata());
		Long parentId = Longs.tryParse(asset.getGoodsId());
		Long nextSeqNo = iTblAssetService.nextSeqNo(parentId, openFake);
		MintRequest mintRequest = MintRequest
				.builder()
				.description(asset.getDescription())
				.externalUrl(fileUri)
				.image(fileUri)
				.name(asset.getAssetName())
				.attributes(mintAttr)
				.authorId(asset.getAuthorId())
				.ownerId(String.valueOf(appUid))
				.referProtocol(assetExt.getReferProtocol())
				.assetType(AssetTypeEnum.DESIGN.name())
				.bizId(IdUtil.getSnowflakeNextIdStr())
				.chainId(Ints.tryParse(assetExt.getChainId()))
				.amount(1)
				.price(asset.getPrice())
				.unit(asset.getUnit())
				.bizType(BizTypeEnum.NFT.dbType())
				.sellEndDatetime(0L)
				.sellEndDatetime(0L)
				.showState(ShowStateEnum.DOWN.code())
				.ownerType(asset.getOwnerType())
				.parentId(parentId)
				.seqNo(nextSeqNo)
				.orderNo(orderNo)
				.build();
		MintResponse mint = iNftService.mint(mintRequest, OrderTypeEnum.NORMAL);
		isNull(mint, ErrorMessage.MINT_EX);
		mint.setSeqNo(nextSeqNo);
		return mint;
	}

	private String fileUri(TblAsset asset) {
		String fileUri = StringUtils.EMPTY;
		List<TblBlindBoxDetail> listCache = iTblBlindBoxDetailService.listCache(asset.getGoodsId());
		if (CollectionUtil.isEmpty(listCache)) {
			TblAssetStaticResource infoWithAssetIdByCache = iTblAssetStaticResourceService.infoWithAssetIdByCache(asset.getId());
			isNull(infoWithAssetIdByCache, ErrorMessage.ASSET_EXIST);
			fileUri = infoWithAssetIdByCache.getDisplayUri();
		}
		int currIndex = ThreadLocalRandom.current().nextInt(0, listCache.size());
		TblBlindBoxDetail blindBoxDetail = listCache.get(currIndex);
		fileUri = blindBoxDetail.getFileUri();
		isTrue(StringUtils.isBlank(fileUri), ErrorMessage.ASSET_EXIST);
		return fileUri;
	}

	private List<MintDataVo> mintAttr(String metadata) {
		if (StringUtils.isNotBlank(metadata)) {
			MetaDataParams bean = JSONUtil.toBean(metadata, MetaDataParams.class);
			List<MintDataVo> mintData = Optional
					.ofNullable(bean)
					.filter(Objects::nonNull)
					.map(MetaDataParams::getAttributes)
					.orElse(Lists.newArrayList())
					.stream()
					.map(x -> new MintDataVo(x.getTrait_type(), String.valueOf(x.getValue())))
					.collect(toList());
			return mintData;
		}
		return Lists.newArrayList();
	}
}